Micron Document
Fox's Git Mirrors

Node / acehoss / wjscli.git rns://e35de29a61974cd952fde3fe9c8500d5/acehoss/wjscli.git


>wjscli

A CLI and MCP server for Wiki.js v2 that authenticates as a real human user β€” by riding the same JWT the browser uses. Page edits are attributed to that user's account; permissions and audit trail stay honest; no admin API key required.

The same set of seven Wiki.js tools is available two ways:

β€’ MCP stdio server (T383838wjscli <url> mcp) β€” for Claude Desktop, MCP Inspector, custom MCP clients.
β€’ CLI (T383838wjscli <url> page get --id 42, T383838wjscli <url> search "needle", etc.) β€” for shells, scripts, and ad-hoc use.

In every invocation, T383838<url> comes first and the subcommand comes second.

Why

β€’ Real user attribution. T383838creatorId and T383838authorId on every page edit point at the actual person, not a shared API key. The audit log is meaningful.
β€’ Permissions come for free. wjscli can only do what the user can do β€” Wiki.js enforces T383838read:pages / T383838write:pages / T383838manage:system server-side against the JWT's T383838groups.
β€’ No admin API key needed. Wiki.js v2's built-in API tokens require admin rights and bypass per-user permissions. wjscli avoids that by riding the regular browser JWT instead.
β€’ Long-running. Wiki.js's JWT lifetime is 30 minutes, but the server emits a T383838new-jwt response header when a token is nearing expiry. wjscli captures that header and updates its stored token, so a process can stay alive indefinitely as long as it makes at least one call per 30 minutes. The T383838validate daemon (T383838-t) polls just often enough to keep the token live across idle stretches.
β€’ stdio MCP. No HTTP listener, no port. Standard MCP transport β€” drops into Claude Desktop and similar clients with one config block.

Install

Not published to npm. Two paths:

Install directly from GitHub (preferred for users who just want to run it):

T282828
npm install -g git+https://github.com/acehoss/wjscli.git

The T383838prepare script in T383838package.json builds T383838dist/ automatically during install, so T383838wjscli lands on your T383838$PATH ready to run.

From a clone (for hacking on it):

T282828
git clone https://github.com/acehoss/wjscli.git
cd wjscli
npm install
npm link

T383838npm link symlinks the working tree's T383838dist/index.js into your global PATH as T383838wjscli. Rebuild (T383838npm run build) and changes are picked up immediately β€” no re-link needed. Remove with T383838npm unlink -g wjscli.

Requires Node β‰₯ 20.

Quick start

1. Install β€” see Install above.
2. Get a JWT from your browser. See Getting a JWT β€” about 10 seconds in DevTools.
3. Validate the JWT and write the config:

T282828
wjscli https://wiki.example.com validate <jwt>

On success this writes T383838~/.config/wjscli/wiki.example.com.json (mode T3838380600) and prints something like:

T282828
Connecting to https://wiki.example.com…
βœ“ Authenticated as Example User <user@example.com> (id=7)
βœ“ Config written to /home/user/.config/wjscli/wiki.example.com.json

4. (Optional, for MCP) Add to your MCP client config. For Claude Desktop, edit T383838~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the equivalent on your platform:

T282828
{
"mcpServers": {
"wikijs": {
"command": "wjscli",
"args": ["https://wiki.example.com", "mcp"]
}
}
}

5. Restart your MCP client. The seven T383838wiki_* tools should appear.

You can also drive the same tools directly:

T282828
wjscli https://wiki.example.com tags list
wjscli https://wiki.example.com search "onboarding"
wjscli https://wiki.example.com page get 42
wjscli https://wiki.example.com page get team/onboarding

For T383838page get, T383838page update, and T383838page history, the positional T383838<id-or-path> is auto-detected: an all-digits value is a numeric Page ID; anything else is treated as a path. T383838page update and T383838page history resolve a path to an id with an extra T383838pages.singleByPath call.

CLI surface

T282828
wjscli <base-url> validate [<jwt>] [-t] Validate / refresh JWT
omit <jwt> to use the cached one
-t: stay running, keep token refreshed
wjscli <base-url> mcp Start MCP stdio server

wjscli <base-url> pages tree [--parent N --mode ALL|PAGES|FOLDERS --locale L --depth N]
wjscli <base-url> page get <id-or-path> [--locale L]
wjscli <base-url> page create --path P --title T --content C [...]
wjscli <base-url> page update <id-or-path> [fields...]
wjscli <base-url> page history <id-or-path> [--offset-page N --offset-size N --locale L]
wjscli <base-url> search <query> [--locale L]
wjscli <base-url> tags list

wjscli sync clone <base-url> <dir> Clone a wiki to local markdown files
wjscli sync status [-C dir] [--remote] Show what changed since last sync
wjscli sync pull [-C dir] [--force] Re-fetch updates from the wiki
wjscli sync push [-C dir] [--force] [--dry-run]
Upload locally-modified pages

wjscli --version
wjscli --help

Add T383838--json to any CLI subcommand for the raw MCP-equivalent JSON payload. Default output is a small human-readable rendering per command (box-drawing tree for T383838pages tree, key/value + content for T383838page get, table-ish for T383838search and T383838page history, etc.). Example:

T282828
$ wjscli https://wiki.example.com pages tree
β”œβ”€β”€ [1] Docs/ (docs)
β”‚ β”œβ”€β”€ [11] Setup (docs/setup)
β”‚ └── [12] API (docs/api)
└── [2] About (about)

T383838pages tree recurses 20 levels by default β€” pass T383838--depth N to limit it. Every subcommand accepts T383838-h / T383838--help for command-specific usage, including options and examples:

Sync (clone the whole wiki to markdown files)

T383838wjscli sync mirrors a Wiki.js instance to a local directory of markdown files with YAML frontmatter, then lets you pull, push, and inspect status from there. It's a git-like working tree of the wiki, useful for agents (everything is a file), bulk edits, and offline editing.

T282828
# initial clone (creates ./wiki/.wjscli/{config,index}.json + one .md per page)
wjscli sync clone https://wiki.example.com ./wiki
cd wiki

# show what changed locally
wjscli sync status

# also check whether the server has changed since last sync (one query per page)
wjscli sync status --remote

# re-fetch from the server; skips files you've edited locally unless --force
wjscli sync pull
wjscli sync pull --force

# upload locally-modified pages; refuses to overwrite if the server's
# \\`updatedAt\\` has drifted since last sync (unless --force)
wjscli sync push
wjscli sync push --dry-run # list what would be pushed without changing anything
wjscli sync push --force # overwrite anyway

Each page becomes a markdown file at the path it lives at in the wiki (T383838team/onboarding β†’ T383838team/onboarding.md). The frontmatter holds the page's metadata (id, path, title, description, tags, locale, isPublished, isPrivate, plus informational createdAt/updatedAt/authorName):

T282828
---
id: 42
path: team/onboarding
title: Onboarding Guide
description: How to get started
locale: en
editor: markdown
isPublished: true
isPrivate: false
tags:
- guide
- onboarding
createdAt: 2026-01-01T00:00:00Z
updatedAt: 2026-05-19T10:42:00Z
authorId: 7
authorName: Aaron Heise
---
# Onboarding Guide

Welcome to the team!

Edit the frontmatter to rename a page (change T383838path:), retag it, mark it private, etc. β€” those go to the wiki on push. The T383838id field is read-only on push (it identifies which page to update; never written as a setting). T383838createdAt, T383838updatedAt, T383838authorId, T383838authorName are informational and ignored on push.

Conflict detection: the T383838.wjscli/index.json records the server's T383838updatedAt at last sync and a SHA-256 of the file content as written. T383838sync status recomputes the hash to spot local edits; T383838sync push re-queries the server's current T383838updatedAt and refuses to overwrite if it has drifted (so two people editing the same page won't silently clobber each other).

T282828
wjscli https://wiki.example.com pages tree --help
wjscli https://wiki.example.com page get -h
wjscli https://wiki.example.com validate --help

T383838page create and T383838page update accept T383838--content - to read content from stdin or T383838--content @path/to/file.md to read from a file. The same T383838@- / T383838@path indirection works for T383838--description.

Boolean toggles use T383838--published / T383838--no-published and T383838--private / T383838--no-private. To pass a value explicitly: T383838--published=true / T383838--published=false.

Tags are repeatable and comma-splittable: T383838--tag a --tag b or T383838--tag a,b (both produce T383838['a', 'b']).

Daemon mode (T383838validate -t)

T383838wjscli <url> validate <jwt> -t does the same one-shot probe as T383838validate, writes the config, and then keeps running β€” polling Wiki.js's lightweight T383838users.profile query every five minutes to give the server a chance to emit a T383838new-jwt refresh header. Any refresh is persisted to the config file atomically. This keeps the stored JWT alive across idle stretches when no MCP or CLI calls are happening.

You can also run T383838wjscli <url> validate -t (no JWT) to start the daemon against the cached token β€” handy when you just want to keep the wheels turning without re-pasting a JWT from your browser.

The daemon exits cleanly on SIGINT/SIGTERM. It also exits (non-zero) if the JWT is rejected β€” at that point only a fresh JWT can recover.

Getting a JWT

Wiki.js stores its session JWT in a cookie that is NOT marked T383838httpOnly (an upstream Wiki.js choice β€” see T383838server/helpers/common.js), so any JS running in the page can read it.

Open DevTools on any authenticated Wiki.js page and paste into the console:

T282828
copy(document.cookie.split('; ').find(c => c.startsWith('jwt=')).slice(4))

The JWT is now on your clipboard. Paste it into the T383838validate command.

If you want a permanent bookmark, save this as a bookmarklet URL (one line, including the T383838javascript: prefix):

T282828
javascript:(()=>{const c=document.cookie.split('; ').find(c=>c.startsWith('jwt='));if(!c){alert('No jwt cookie on this page β€” are you logged in to Wiki.js?');return;}navigator.clipboard.writeText(c.slice(4));alert('JWT copied to clipboard');})();

Click the bookmark while viewing a logged-in Wiki.js tab to copy the current JWT to your clipboard.

Security notes:

β€’ The JWT is a bearer credential. Anyone who has it can act as you against Wiki.js for the remaining lifetime of the token (default 30 minutes from the most recent refresh).
β€’ wjscli stores it on disk in plaintext at mode T3838380600 (user-readable only). The containing directory is mode T3838380700.
β€’ Treat it like a password: don't paste it into chat, don't commit it, don't email it.

MCP tools

After validation and (re)start, your MCP client sees these seven tools:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Tool β”‚ What it does β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ T383838wiki_pages_tree β”‚ List a slice of the Wiki.js page tree under a parent node. Each entry inclu… β”‚
β”‚ T383838wiki_page_get β”‚ Fetch a single Wiki.js page by T383838id or by T383838{path, locale?} (exactly one β€” neve… β”‚
β”‚ T383838wiki_page_create β”‚ Create a Wiki.js page. Required: T383838path, T383838title, T383838content. Defaults applied for… β”‚
β”‚ T383838wiki_page_update β”‚ Update a Wiki.js page. Requires T383838id plus at least one field to change. Only … β”‚
β”‚ T383838wiki_search β”‚ Search Wiki.js pages. Returns T383838{ results, suggestions, totalHits } as Wiki.j… β”‚
β”‚ T383838wiki_tags_list β”‚ List all tags across pages the calling user can read. Filtered server-side … β”‚
β”‚ T383838wiki_page_history β”‚ Fetch the revision history of a Wiki.js page. Paginated via T383838offsetPage (def… β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Tool outputs are pretty-printed JSON in a single MCP T383838text content block. Tool-execution errors (HTTP, network, GraphQL) come back as T383838{ isError: true, content: [...] } with a T383838{ code, message } JSON payload β€” agents see the failure as a normal tool result they can react to, not as a transport-layer error.

The CLI subcommands listed above wrap exactly these seven tools.

Configuration

β€’ Location: T383838${XDG_CONFIG_HOME:-$HOME/.config}/wjscli/<host>.json, where T383838<host> is the URL host (lowercased, including a non-default port if any). Examples: T383838wiki.example.com.json, T383838wiki.example.com:8443.json.
β€’ File mode: T3838380600. Containing dir mode: T3838380700.
β€’ Override the config dir with the T383838WJSCLI_CONFIG_DIR env var (mostly useful for tests).
β€’ Schema: T383838{ baseUrl, jwt, refreshedAt, note? } β€” see SPEC.md for the field list.

The T383838validate command writes this file atomically (tmp file + fsync + rename), so a crashed T383838validate never leaves a corrupt config.

How it stays alive

Wiki.js v2's JWT lifetime is 30 minutes from issue. The server emits a T383838new-jwt response header on any authenticated T383838Content-Type: application/json request when the token is in its renewal window. wjscli captures that header, updates the in-memory token, and atomically writes the new value back to the config file (debounced ~250 ms).

In practice this means:

β€’ If your MCP client uses the wiki at least once every 30 minutes, the JWT stays refreshed indefinitely.
β€’ For idle stretches, run T383838wjscli <url> validate <jwt> -t in a separate shell as a daemon β€” it polls every 5 minutes to keep the refresh window covered.
β€’ If the JWT expires beyond auto-refresh range, the next call gets an auth-rejection from Wiki.js β†’ wjscli surfaces that as an T383838AuthExpiredError with re-validate guidance.
β€’ A file watcher on the config also notices if you re-validate from another shell β€” a running MCP or daemon picks up the new JWT live without needing to restart.

Re-validating

When you see a tool call fail with T383838JWT rejected by server or T383838Wiki.js rejected the JWT for <url>, the stored token has expired beyond auto-refresh range. Two-step fix:

1. Grab a fresh JWT (DevTools console or bookmarklet β€” see Getting a JWT).
2. Re-run T383838wjscli <base-url> validate <jwt>.

The running MCP server (or daemon) picks up the new config automatically (file watcher); you don't need to restart your MCP client unless a tool call was in flight.

Limitations (v1)

β€’ No assets/uploads. Page bodies only.
β€’ No page move/delete. Read and write to existing or new paths only.
β€’ No admin/user management. Per-user permissions apply, but you can't manage users/groups through wjscli.
β€’ One base URL per process. If you need access to two Wiki.js instances, run two processes / use two configs.
β€’ T383838pages.search doesn't expose its T383838path prefix filter. Just T383838query and T383838locale.

Security

β€’ The JWT is a bearer credential. Anyone holding it can act as you against Wiki.js until it expires.
β€’ Stored at mode T3838380600 in your user config dir. The directory is T3838380700. Don't copy the file or share its contents.
β€’ The JWT is never logged by wjscli β€” error messages from the underlying HTTP / GraphQL client are run through a redactor that scrubs JWT-shaped substrings before they reach any output. Tool result content carries only the structured T383838{code, message}, never the token.
β€’ A new-jwt refresh debounces a write to disk; on process shutdown (SIGINT/SIGTERM/normal exit) any pending write is flushed before exit.
β€’ T383838stdout is reserved for MCP stdio framing (in T383838mcp mode) and for CLI command output. All T383838validate output and all error messages go to T383838stderr.

Development

After cloning (see Install for the T383838npm link flow), the standard workflow:

T282828
npm install # also runs \\`prepare\\` β†’ builds dist/
npm run build # tsc + chmod 0755 on dist/index.js
npm test # vitest, hermetic (no live Wiki.js needed)
npm run typecheck
npm run lint
node dist/index.js --help

The full design lives in SPEC.md. Tests are organized as:

β€’ T383838test/config.test.ts β€” atomic config file writes, URL canonicalization.
β€’ T383838test/token-store.test.ts β€” JWT lifecycle, debounced disk writes, file watcher.
β€’ T383838test/wiki/client.test.ts β€” HTTP / GraphQL / network classification, JWT redaction, retry policy, T383838new-jwt capture.
β€’ T383838test/wiki/queries.test.ts β€” GraphQL document pin-tests.
β€’ T383838test/validate.test.ts β€” T383838wjscli <url> validate (with and without T383838-t) end-to-end against an in-process Wiki.js mock.
β€’ T383838test/server.test.ts β€” T383838wjscli <url> mcp startup, signal handling.
β€’ T383838test/index.test.ts β€” top-level argv dispatch.
β€’ T383838test/cli/argv.test.ts β€” CLI argv reader unit tests.
β€’ T383838test/cli/run.test.ts β€” CLI dispatch end-to-end through T383838runCli against the mock.
β€’ T383838test/tools/*.test.ts β€” per-tool happy / Zod / auth-expired / GraphQL paths via T383838dispatchTool directly.
β€’ T383838test/tools/e2e.test.ts β€” one round trip per tool through real T383838Server + T383838Client over an in-memory transport pair, plus JWT-refresh integration.
β€’ T383838test/mock/graphql-server.ts β€” typed mock backend used by everything above except the standalone client tests.

Reporting issues

This project doesn't yet have a public issues URL. If you've got it from someone, send your feedback to whoever shared it.

Served by rngit 1.4.1 - Generated in 0.1s